import * as fs from "node:fs"
import * as path from "node:path"
import { KAVLTree, KHashTable, KMap, numcmp, numhash, pad0 } from "@coldcloude/kai2";
import { pixel, Pixel, TRANSPARENT } from "../hp3-img.js";
import { savePng } from "../hp3-png.js";

export type GraphicInfo = {
    id: number,
    offset: number,
    length: number,
    positionX: number,
    positionY: number,
    width: number,
    height: number,
    gridWidth: number,
    gridHeight: number,
    mapFlag: number,
    mapId: number
}

export type GraphicHeader = {
    version: number,
    width: number,
    height: number,
    length: number
}

export type Graphic = {
    header: GraphicHeader,
    pixels: Pixel[]
}

export type AnimeInfo = {
    id: number,
    offset: number,
    total: number
}

export type SingleAnimeHeader = {
    direction: number,
    action: number,
    duration: number,
    length: number
}

export type SingleAnime = {
    header: SingleAnimeHeader,
    frames: number[]
}

export type Anime = {
    info: AnimeInfo,
    singles: SingleAnime[]
}

export type Map = {
    id: string,
    width: number,
    height: number,
    tiles: number[],
    objects: number[],
    tags: number[],
    flags: number[],
    tileGraphics: GraphicInfo[],
    objectGraphics: GraphicInfo[]
}

function skip(fd:number,length:number,buffer:Buffer<ArrayBuffer>){
    let rest = length;
    while(rest>0){
        const batch = Math.min(rest,buffer.length);
        const r = fs.readSync(fd,buffer,0,batch,-1);
        if(r!==batch){
            return -1;
        }
        else{
            rest -= r;
        }
    }
    return length;
}

const CNT_PALET = 224
const LEN_PALET = CNT_PALET*3;

const PALET_FIRSTS = [
    [0x00,0x00,0x00],
    [0x00,0x00,0x80],
    [0x00,0x80,0x00],
    [0x00,0x80,0x80],
    [0x80,0x00,0x00],
    [0x80,0x00,0x80],
    [0x80,0x80,0x00],
    [0xC0,0xC0,0xC0],
    [0xC0,0xDC,0xC0],
    [0xF0,0xCA,0xA6],
    [0x00,0x00,0xDE],
    [0x00,0x5F,0xFF],
    [0xA0,0xFF,0xFF],
    [0xD2,0x5F,0x00],
    [0xFF,0xD2,0x50],
    [0x28,0xE1,0x28]
]

const PALET_LASTS = [
    [0x96,0xC3,0xF5],
    [0x5F,0xA0,0x1E],
    [0x46,0x7D,0xC3],
    [0x1E,0x55,0x9B],
    [0x37,0x41,0x46],
    [0x1E,0x23,0x28],
    [0xF0,0xFB,0xFF],
    [0xA5,0x6E,0x3A],
    [0x80,0x80,0x80],
    [0x00,0x00,0xFF],
    [0x00,0xFF,0x00],
    [0x00,0xFF,0xFF],
    [0xFF,0x00,0x00],
    [0xFF,0x80,0xFF],
    [0xFF,0xFF,0x00],
    [0xFF,0xFF,0xFF]
]

export function parsePalet(file:string):KHashTable<number,Pixel>{
    const rst = new KHashTable<number,Pixel>(numcmp,numhash);
    for(let i=0; i<PALET_FIRSTS.length; i++){
        rst.set(i,pixel(PALET_FIRSTS[i]));
    }
    const fd = fs.openSync(file,"r")
    try{
        const buffer = Buffer.alloc(LEN_PALET)
        const r = fs.readSync(fd,buffer,0,LEN_PALET,-1);
        if(r==LEN_PALET){
            for(let i=0; i<CNT_PALET; i++){
                const base = i*3;
                const r = buffer.readUInt8(base+2);
                const g = buffer.readUInt8(base+1);
                const b = buffer.readUInt8(base);
                rst.set(i+16,pixel([r,g,b]));
            }
        }
    }
    catch(e){
        console.error(e);
    }
    fs.closeSync(fd)
    for(let i=0; i<PALET_LASTS.length; i++){
        rst.set(i+240,pixel(PALET_LASTS[i]));
    }
    return rst;
}

function getPixel(palet:KMap<number,Pixel>,pp:number):Pixel{
    const p = palet.get(pp);
    return p===undefined?TRANSPARENT:p;
}

const LGI_ID = 4;
const LGI_OFFSET = 4;
const LGI_LENGTH = 4;
const LGI_POSITION_X = 4;
const LGI_POSITION_Y = 4;
const LGI_WIDTH = 4;
const LGI_HEIGHT = 4;
const LGI_GRID_WIDTH = 1;
const LGI_GRID_HEIGHT = 1;
const LGI_MAP_FLAG = 1;
const LGI_RESERVED = 5;
const LGI_RESERVED_LEGACY = 45;

const LEN_GRAPHIC_INFO = 40;
const LEN_GRAPHIC_INFO_LEGACY = 80;

export function parseAllGraphicInfo(file:string,legacy?:boolean):GraphicInfo[]{
    const size = legacy?LEN_GRAPHIC_INFO_LEGACY:LEN_GRAPHIC_INFO;
    const reserved = legacy?LGI_RESERVED_LEGACY:LGI_RESERVED;
    const rst:GraphicInfo[] = [];
    const fd = fs.openSync(file,"r")
    try{
        const buffer = Buffer.allocUnsafe(size)
        let r;
        while((r=fs.readSync(fd,buffer,0,size,-1))===size){
            let index = 0;
            const id = buffer.readInt32LE(index);
            index += LGI_ID;
            const offset = buffer.readInt32LE(index);
            index += LGI_OFFSET;
            const length = buffer.readInt32LE(index);
            index += LGI_LENGTH;
            const positionX = buffer.readInt32LE(index);
            index += LGI_POSITION_X;
            const positionY = buffer.readInt32LE(index);
            index += LGI_POSITION_Y;
            const width = buffer.readInt32LE(index);
            index += LGI_WIDTH;
            const height = buffer.readInt32LE(index);
            index += LGI_HEIGHT;
            const gridWidth = buffer.readUInt8(index);
            index += LGI_GRID_WIDTH;
            const gridHeight = buffer.readUInt8(index);
            index += LGI_GRID_HEIGHT;
            const mapFlag = buffer.readUInt8(index);
            index += LGI_MAP_FLAG+reserved;
            const mapId = buffer.readInt32LE(index);
            rst.push({
                id: id,
                offset: offset,
                length: length,
                positionX: positionX,
                positionY: positionY,
                width: width,
                height: height,
                gridWidth: gridWidth,
                gridHeight: gridHeight,
                mapFlag: mapFlag,
                mapId: mapId
            });
        }
    }
    catch(e){
        console.error(e);
    }
    fs.closeSync(fd)
    return rst;
}

const LG_MAGIC = 2;
const LG_VERSION = 1;
const LG_RESERVED = 1;
const LG_WIDTH = 4;
const LG_HEIGHT = 4;

const LEN_GRAPHIC_HEADER = 16;

class PixelParser {
    palet:KMap<number,Pixel>;
    fd: number;
    seek: number;
    buffer:Buffer<ArrayBuffer>;
    rest:number;
    version:number;
    batch = 0;
    offset = -1;
    currMode = -1;
    currSize = 0;
    currIndex = 0;
    currPixel:Pixel = TRANSPARENT;
    constructor(palet:KMap<number,Pixel>,fd:number,seek:number,buffer:Buffer<ArrayBuffer>,rest:number,version:number){
        this.palet = palet;
        this.fd = fd;
        this.seek = seek;
        this.buffer = buffer;
        this.rest = rest;
        this.version = version;
    }
    _load(){
        this.batch = Math.min(this.buffer.length,this.rest);
        if(this.batch>0){
            const r = fs.readSync(this.fd,this.buffer,0,this.batch,this.seek);
            if(r===this.batch){
                if(this.seek>=0){
                    this.seek += this.batch;
                }
                this.rest -= this.batch;
                this.offset = 0;
            }
            else{
                this.batch = -1;
            }
        }
    }
    _clearCurr(){
        this.currMode = -1;
        this.currSize = 0;
        this.currIndex = 0;
        this.currPixel = TRANSPARENT;
    }
    parse(){
        const pixels:Pixel[] = [];
        const compress = (this.version&0x01)>0;
        this._load();
        while(this.batch>0){
            if(compress){
                while(this.offset<this.batch){
                    let curr = this.buffer.readUint8(this.offset);
                    if(this.currMode<0){
                        //new segment
                        const mode = (curr>>4)&0x0F;
                        if(mode>=0x00&&mode<=0x02||mode>=0x08&&mode<=0x0A||mode>=0x0C&&mode<=0x0E){
                            //compress
                            this.currMode = mode;
                            this.currSize = curr&0x0F;
                            this.currIndex = 0;
                        }
                        else{
                            //not compress
                            pixels.push(getPixel(this.palet,curr));
                        }
                        this.offset++;
                    }
                    else if(this.currMode>=0&&this.currMode<=3){
                        //direct segment
                        if(this.currIndex<this.currMode){
                            //read size
                            this.currSize = (this.currSize<<8)|curr;
                            this.currIndex++;
                            this.offset++;
                        }
                        else{
                            //read pixel
                            const p = getPixel(this.palet,curr);
                            pixels.push(p);
                            this.currIndex++;
                            this.offset++;
                        }
                    }
                    else if(this.currMode>=0x08&&this.currMode<=0x0A){
                        //same pixel segment
                        if(this.currIndex===0){
                            //read pixel
                            this.currPixel = getPixel(this.palet,curr);
                            this.currIndex++;
                            this.offset++;
                        }
                        else if(this.currIndex-1<this.currMode-0x08){
                            //read size
                            this.currSize = (this.currSize<<8)|curr;
                            this.currIndex++;
                            this.offset++;
                        }
                    }
                    else if(this.currMode>=0x0C&&this.currMode<=0x0E){
                        //transparent pixel
                        if(this.currIndex<this.currMode-0x0C){
                            //read size
                            this.currSize = (this.currSize<<8)|curr;
                            this.currIndex++;
                            this.offset++;
                        }
                    }
                    //check and finish segment
                    if(this.currMode>=0&&this.currMode<=3){
                        //end of segment
                        if(this.currIndex-this.currMode===this.currSize){
                            this._clearCurr();
                        }
                    }
                    else if(this.currMode>=0x08&&this.currMode<=0x0A){
                        //finish segment
                        if(this.currIndex-1==this.currMode-0x08){
                            for(let i=0; i<this.currSize; i++){
                                pixels.push(this.currPixel);
                            }
                            this._clearCurr();
                        }
                    }
                    else if(this.currMode>=0x0C&&this.currMode<=0x0E){
                        //finish segment
                        if(this.currIndex==this.currMode-0x0C){
                            for(let i=0; i<this.currSize; i++){
                                pixels.push(TRANSPARENT);
                            }
                            this._clearCurr();
                        }
                    }
                }
            }
            else{
                //not compress
                for(; this.offset<this.batch; this.offset++){
                    const pp = this.buffer.readUInt8(this.offset);
                    const p = getPixel(this.palet,pp);
                    pixels.push(p);
                }
            }
            this._load();
        }
        return pixels;
    }
}

export function parseGraphic(palet:KMap<number,Pixel>,fd:number,seek:number,buffer:Buffer<ArrayBuffer>):Graphic{
    const rh = fs.readSync(fd,buffer,0,LEN_GRAPHIC_HEADER,seek);
    if(rh!==LEN_GRAPHIC_HEADER){
        throw "read header failed";
    }
    //read header
    let index = LG_MAGIC;
    const version = buffer.readUInt8(index);
    index += LG_VERSION+LG_RESERVED;
    const width = buffer.readInt32LE(index);
    index += LG_WIDTH;
    const height = buffer.readInt32LE(index);
    index += LG_HEIGHT;
    const length = buffer.readInt32LE(index);
    if(seek>=0){
        seek += LEN_GRAPHIC_HEADER;
    }
    if(width<0||height<0||width>65535||height>65535){
        throw "wrong size: ("+width+","+height+")";
    }
    //read pixels
    const pixels = new PixelParser(palet,fd,seek,buffer,length-LEN_GRAPHIC_HEADER,version).parse();
    //reverse y axis
    const rps:Pixel[] = [];
    for(let y=0; y<height; y++){
        for(let x=0; x<width; x++){
            const p = pixels[(height-y-1)*width+x];
            rps.push(p===undefined?TRANSPARENT:p);
        }
    }
    return {
        header: {
            version: version,
            width: width,
            height: height,
            length: length
        },
        pixels: rps
    };
}

export function saveAllGraphic(palet:KMap<number,Pixel>,infos:GraphicInfo[],file:string,output:string,start:number,end:number){
    const infoMap = new KAVLTree<number,GraphicInfo>(numcmp);
    for(const info of infos){
        if((start<0||info.id>=start)&&(end<0||info.id<end)){
            infoMap.set(info.offset,info);
        }
    }
    const fd = fs.openSync(file,"r");
    try{
        const buffer = Buffer.allocUnsafe(4096);
        let count = 0;
        // let offset = 0;
        infoMap.foreach((o:number,info:GraphicInfo)=>{
            try{
                // const s = o-offset;
                // if(s>0){
                //     const r = skip(fd,s,buffer);
                //     if(r===s){
                //         offset += r;
                //     }
                //     else{
                //         return true;
                //     }
                // }
                // if(offset===o){
                    const graphic = parseGraphic(palet,fd,o,buffer);
                    // offset += graphic.header.length;
                    const part = Math.floor(info.id/1000)|0;
                    const dir = path.join(output,pad0(part,3));
                    const file = path.join(dir,pad0(info.id,6)+".png");
                    if(!fs.existsSync(dir)){
                        fs.mkdirSync(dir,{recursive:true});
                    }
                    fs.mkdirSync(path.dirname(file),{recursive:true});
                    savePng(graphic.header.width,graphic.header.height,graphic.pixels,file);
                // }
            }
            catch(e){
                console.error(e);
                return true;
            }
            count++;
            if(count%1000===0){
                console.log("id="+info.id+" processed");
            }
        });
    }
    catch(e){
        console.error(e);
    }
    fs.closeSync(fd);
}

const LAI_ID = 4;
const LAI_OFFSET = 4;

const LEN_ANIME_INFO = 12;

export function parseAllAnimeInfo(file:string):AnimeInfo[]{
    // const rst = new KAVLTree<number,AnimeInfo>(numcmp);
    const rst:AnimeInfo[] = [];
    const fd = fs.openSync(file,"r")
    try{
        const buffer = Buffer.allocUnsafe(LEN_ANIME_INFO)
        let r;
        while((r=fs.readSync(fd,buffer,0,LEN_ANIME_INFO,-1))===LEN_ANIME_INFO){
            let index = 0;
            const id = buffer.readInt32LE(index);
            index += LAI_ID;
            const offset = buffer.readInt32LE(index);
            index += LAI_OFFSET;
            const total = buffer.readInt16LE(index);
            const r:AnimeInfo = {
                id: id,
                offset: offset,
                total: total
            };
            // rst.set(id,r);
            rst.push(r);
        }
    }
    catch(e){
        console.error(e);
    }
    fs.closeSync(fd)
    // return rst.valueToArray();
    return rst;
}

const LA_DIRECTION = 2;
const LA_ACTION = 2;
const LA_DURATION = 4;

const LEN_ANIME_HEADER = 12;

const LEN_ANIME_FRAME = 10;

class FrameParser {
    fd: number;
    seek: number;
    buffer:Buffer<ArrayBuffer>;
    rest:number;
    batch = 0;
    offset = -1;
    length = 0;
    index = 0;
    constructor(fd:number,seek:number,buffer:Buffer<ArrayBuffer>,rest:number){
        this.fd = fd;
        this.seek = seek;
        this.buffer = buffer;
        this.rest = rest;
    }
    _load(){
        this.batch = Math.min(this.buffer.length,this.rest);
        this.batch = (Math.floor(this.batch/10)*10)|0;
        if(this.batch>0){
            const r = fs.readSync(this.fd,this.buffer,0,this.batch,this.seek);
            if(r===this.batch){
                if(this.seek>=0){
                    this.seek += this.batch;
                }
                this.rest -= this.batch;
                this.offset = 0;
            }
            else{
                this.batch = -1;
            }
        }
    }
    _clearCurr(){
        this.length = 0;
        this.index = 0;
    }
    parse(){
        const frames:number[] = [];
        this._load();
        while(this.batch>0){
            //not compress
            for(; this.offset<this.batch; this.offset+=LEN_ANIME_FRAME){
                const f = this.buffer.readInt32LE(this.offset);
                frames.push(f);
            }
            this._load();
        }
        return frames;
    }
}

export function parseSingleAnime(fd:number,seek:number,buffer:Buffer<ArrayBuffer>):SingleAnime{
    const rh = fs.readSync(fd,buffer,0,LEN_ANIME_HEADER,seek);
    if(rh!==LEN_ANIME_HEADER){
        throw "read header failed";
    }
    //read header
    let index = 0;
    const direction = buffer.readInt16LE(index);
    index += LA_DIRECTION;
    const action = buffer.readInt16LE(index);
    index += LA_ACTION;
    const duration = buffer.readInt32LE(index);
    index += LA_DURATION;
    const length = buffer.readInt32LE(index);
    if(seek>=0){
        seek += LEN_GRAPHIC_HEADER;
    }
    //read frames
    const frames = new FrameParser(fd,seek,buffer,LEN_ANIME_FRAME*length).parse();
    return {
        header: {
            direction: direction,
            action: action,
            duration: duration,
            length: length
        },
        frames: frames
    };
}

export function parseAllAnime(infos:AnimeInfo[],file:string,start:number,end:number):Anime[]{
    const infoMap = new KAVLTree<number,AnimeInfo>(numcmp);
    for(const info of infos){
        if((start<0||info.id>=start)&&(end<0||info.id<end)){
            infoMap.set(info.offset,info);
        }
    }
    const rst:Anime[] = [];
    const fd = fs.openSync(file,"r");
    try{
        const buffer = Buffer.allocUnsafe(2000);
        let count = 0;
        let offset = 0;
        infoMap.foreach((o:number,info:AnimeInfo)=>{
            let index = 0;
            try{
                const s = o-offset;
                if(s>0){
                    const r = skip(fd,s,buffer);
                    if(r===s){
                        offset += r;
                    }
                    else{
                        return true;
                    }
                }
                if(offset===o){
                    const anime:Anime = {
                        info: info,
                        singles: []
                    };
                    for(; index<info.total; index++){
                        const single = parseSingleAnime(fd,-1,buffer);
                        anime.singles.push(single);
                        offset += LEN_ANIME_HEADER+LEN_ANIME_FRAME*single.header.length;
                    }
                    rst.push(anime);
                }
            }
            catch(e){
                console.error(JSON.stringify(info)+" index="+index+" parse error: "+e);
                return true;
            }
            count++;
            if(count%100===0){
                console.log("id="+info.id+" processed");
            }
        });
    }
    catch(e){
        console.error(e);
    }
    fs.closeSync(fd);
    return rst;
}

const LM_HEADER = 12;
const LM_WIDTH = 4;
const LM_HEIGHT = 4;
const LM_GRID = 2;

export function parseMap(graphicInfos:GraphicInfo[],id:string,mapFile:string,legacy?:boolean):Map{
    const graphicMap = new KHashTable<number,GraphicInfo>(numcmp,numhash);
    for(const gi of graphicInfos){
        const mapId = gi.mapId;
        if(mapId>0){
            graphicMap.set(mapId,gi);
        }
    }
    const buffer = fs.readFileSync(mapFile);
    let offset = !legacy?LM_HEADER:0;
    const width = buffer.readInt32LE(offset);
    offset += LM_WIDTH;
    const height = buffer.readInt32LE(offset);
    offset += LM_HEIGHT;
    const flags:number[] = [];
    for(let y=0; y<height; y++){
        for(let x=0; x<width; x++){
            flags.push(0);
        }
    }
    const tileMap = new KAVLTree<number,GraphicInfo>(numcmp);
    const tiles:number[] = [];
    for(let y=0; y<height; y++){
        for(let x=0; x<width; x++){
            const mapId = buffer.readInt16LE(offset);
            offset += LM_GRID;
            tiles.push(mapId);
            if(mapId>0){
                const gi = graphicMap.get(mapId);
                if(gi!==undefined){
                    tileMap.set(gi.id,gi);
                    const i = y*width+x;
                    flags[i] = gi.mapFlag;
                }
            }
        }
    }
    const objectMap = new KAVLTree<number,GraphicInfo>(numcmp);
    const objects:number[] = [];
    for(let y=0; y<height; y++){
        for(let x=0; x<width; x++){
            const mapId = buffer.readInt16LE(offset);
            offset += LM_GRID;
            objects.push(mapId);
            if(mapId>0){
                const gi = graphicMap.get(mapId);
                if(gi!==undefined){
                    objectMap.set(gi.id,gi);
                    const i = y*width+x;
                    flags[i] &= gi.mapFlag;
                }
            }
        }
    }
    const tags:number[] = [];
    for(let y=0; y<height; y++){
        for(let x=0; x<width; x++){
            const tag = buffer.readInt16LE(offset);
            offset += LM_GRID;
            tags.push(tag);
        }
    }
    return {
        id: id,
        width: width,
        height: height,
        tiles: tiles,
        objects: objects,
        tags: tags,
        flags: flags,
        tileGraphics: tileMap.valueToArray(),
        objectGraphics: objectMap.valueToArray()
    };
}
